只是個幫助忙碌的專業人士和父母找回時間、平衡生活的斜槓老爸。 我探索人生的大小賽局,分享優化人生的實用觀點(關於人類、科技和未來)。
在我的個人網站上獲取最新的觀點:https://klkuo.guru
在後端學習的過程中,不時需要復刻我們生活中常使用的工具,來刻意練習。雖然 MongoDB 是 NoSQL 的資料庫,但現實中非常多的概念是互相關聯的。以記帳為例,每筆記錄都對應到一個類別,而每個類別下都包含了數筆不同的紀錄。那在建置資料庫及資料操作時,究竟如何設計,能大幅度提高資料庫的效能,而不重複存放非常多相同名稱的類別資料呢?
為了解決此問題,在作業中我嘗試使用 MongoDB 搭配 Mongoose 來攻略關聯式資料庫的建置與操作。
本篇筆記將解決以下問題:
.populate()
方法如何協助我們運用關聯資料?誰適合閱讀:
參考資料:
本篇筆記將依照 產品工匠日常:打造全端產品的宏觀程序 中,資料庫架設及功能實作的架構及順序來記錄,我如何設計 Data Model(Schema)及以 .populate()
關聯資料,僅摘錄部分程式碼,細節請參考 GitHub repo:
關聯部份設定路徑,用以存放對應資料的 ObjectId:
models/category.js
const categorySchema = new Schema({
title: {
type: String,
trim: true,
required: true
},
icon: {
type: String,
trim: true,
required: true
},
records: [{
type: Schema.Types.ObjectId,
ref: 'Record'
}]
})
module.exports = mongoose.model('Category', categorySchema)
models/record.js
const recordSchema = new Schema({
name: {
type: String,
trim: true,
required: true
},
category: {
type: Schema.Types.ObjectId,
ref: 'Category'
},
date: {
type: String,
required: true
},
amount: {
type: Number,
min: [1, 'at least one dollar'],
required: true
}
})
module.exports = mongoose.model('Record', recordSchema)
先新增類別,再新增種子記錄,以撈取對應的類別。比較麻煩的部分是我想兩個 collection 都更新對應資料,所以有兩層的資料操作:
$ node models/seeds/categorySeeder.js
const categories = [
['家居物業', 'fa-home'],
['交通出行', 'fa-shuttle-van'],
['休閒娛樂', 'fa-grin-beam'],
['餐飲食品', 'fa-utensils'],
['其他', 'fa-pen']
].map(category => ({
title: category[0],
icon: `<i class="fas ${category[1]}"></i>`
}))
// Generate category seed
db.once('open', () => {
Category.create(categories)
.then(() => {
db.close()
})
console.log('categorySeeder.js done ^_^')
})
$ node models/seeds/recordSeeder.js
db.once('open', () => {
createRecords()
console.log('recordSeeder.js done ^_^')
})
function createRecords() {
Category.find()
.then(categories => {
const categoriesId = []
categories.forEach(category => {
categoriesId.push(category._id)
})
return categoriesId // 含有所有類別 _id 的 array
})
.then(id => {
for (let i = 0; i < 5; i++) {
Record.create({ // 新增紀錄
name: `name-${i}`,
category: id[i],
date: `2020-09-0${i + 1}`,
amount: (i + 1) * 100
})
.then(record => { // 將對應紀錄存入類別的 collection 中
Category.findById(id[i])
.then(category => {
category.records.push(record._id)
category.save()
})
})
}
})
.catch(error => console.error(error))
}
最複雜的部分屬 CRUD 的操作,因為想同步 records 和 categories 的 collections。
P.s. 後來在 MDN 上才發現,其實也不一定要同步,可以統一更新在其中一個 collection 在使用 .populate()
關聯即可。
// routes/modules/records.js
router.post('/new', (req, res) => {
const record = req.body // 整筆紀錄存放在 object 中
Category.findOne({ title: record.category })
.then(category => {
record.category = category._id // 找到對應的 category._id
Record.create(record) // 新增紀錄
.then(record => {
category.records.push(record._id) // 更新 categories collection 中對應的類別
category.save()
})
.then(() => res.redirect('/'))
.catch(error => console.error(error))
})
.catch(error => console.error(error))
})
// routes/modules/home.js
router.get('/', (req, res) => {
Category.find()
.lean()
.sort({ _id: 'asc' })
.then(checkedCategories => {
Record.find()
.populate('category') // 將所有紀錄的類別關聯 categories collection
.lean()
.sort({ _id: 'asc' })
.then(records => {
let totalAmount = 0
records.forEach(record => totalAmount += record.amount)
res.render('index', { records, totalAmount, checkedCategories })
})
.catch(error => console.error(error))
})
.catch(error => console.error(error))
})
// routes/modules/records.js
router.put('/:id', (req, res) => {
const { id } = req.params
const update = req.body
// remove this record from old category
Record.findById(id)
.then(record => {
Category.findById(record.category)
.then(category => {
category.records = category.records.filter(record => record.toString() !== id)
category.save()
})
.catch(error => console.error(error))
})
.catch(error => console.error(error))
// assign category id in update object
Category.findOne({ title: update.category })
.then(category => {
update.category = category._id
// update record
Record.findByIdAndUpdate(id, update, { new: true })
.then(record => {
category.records.push(record._id)
category.save()
})
.then(() => res.redirect(`/`))
.catch(error => console.error(error))
})
.catch(error => console.error(error))
})
// routes/modules/records.js
router.delete('/:id', (req, res) => {
const { id } = req.params
Record.findById(id)
.then(record => {
Category.findById(record.category)
// remove record from collection of category
.then(category => {
category.records = category.records.filter(record => record.toString() !== id)
category.save()
})
.catch(error => console.error(error))
// delete this record
record.remove()
})
.then(() => res.redirect('/'))
.catch(error => console.error(error))
})
關於本系列更多內容及導讀,請閱讀作者於 Medium 個人專欄 【無限賽局玩家 Infinite Gamer | Publication – 】 上的文章 《用 JavaScript 打造全端產品的入門學習筆記》系列指南。